写一个[[005.干掉代码中的console.log|干掉代码中的console.log]] 的插件

Note

仅用作原理学习和面试吹牛 B,真实项目请使用现成的轮子

想要亲自动手写一个 Webpack 的插件,那么不得不聊一下 Webpack 的构建过程。看看 plugin 是在什么阶段生效的,或者说 plugin 能生效的阶段都有哪些

Webpack 的构建过程

Webpack 的构建过程可以分为以下几个主要步骤:(下方的内容会结合 create-react-app 这个脚手架去聊,以下简称为 CRA)

初始化阶段 (Initialization)

启动

Webpack 通过 CLI 或 API 启动,并读取配置文件(如 webpack.config.js)。在 webpack.config.js 会存在判断当前环境的代码:

const isEnvDevelopment = webpackEnv === "development";
const isEnvProduction = webpackEnv === "production";

并且在文件内容中,很多配置的地方,都会找到一些三元表达式:

![[Pasted image 20250711102302.png]]

也就是说,同一个 plugin,根据不同的环境,可能存在不同的配置,这是为什么呢?

这其实就和 Webpack 的优化有关了。通过区分环境(开发环境、生产环境),来实现优化:

在 CRA 中,配置文件不止一个,还存在另一个 webpackDevServer.config.js 文件,它又是做什么用的?

它是用于配置开发服务器 webpack-dev-server 的行为。这里也拿一些配置项举几个例子,方便理解:

创建 Compiler 对象

Webpack 初始化一个 Compiler 对象,该对象负责控制整个构建过程

加载插件

Webpack 读取配置中的插件,并调用插件的 apply 方法,让插件可以注册钩子函数

一个 plugin 的基本结构:

class DemoPlugin {
  // options: 接收 plugin 的配置项
  constructor(options) {
    // 获取配置项,初始化插件
  }

  // apply 是与 webpack 通信的桥梁
  apply(compiler) {
    // 获取 compiler,可以通过 compiler 对象访问 compilation 对象
  }
}

apply 内部可以包含任何自定义逻辑,这些逻辑将在 Webpack 的特定生命周期钩子被触发时执行。插件可以利用这些钩子来修改构建结果、添加新的资产、或者执行其他任何必要的操作

比如:

apply(compiler) {
  compiler.hooks.compile.tap('DemoPlugin', (compilationParams) => {
    // 在编译器开始读取 Records 之前执行的操作
  });

  compiler.hooks.compilation.tap('DemoPlugin', (compilation) => {
    // 在创建新的 compilation 之前执行的操作
  });
}

编译阶段 (Compilation)

确定入口

Webpack 根据配置中的 entry 找到所有入口文件

创建 Compilation 对象

每当检测到文件变化时,Webpack 都会创建一个新的 Compilation 对象,该对象包含了当前的模块资源、编译生成资源、变化的文件等

编译模块

生成资源阶段 (Make)

优化阶段 (Seal)

发射阶段 (Emit)

完成阶段 (After)

完成阶段也可以让 plugin 介入,只要在 apply 中注册 hook 即可:

apply(compiler) {
 // 注册完成阶段的钩子
 compiler.hooks.done.tap('AfterBuildPlugin', (stats) => {
   // 完成通知
   console.log('Webpack build is finished!');

   // 清理工作
   this.cleanup();
 });
}

创建自定义 Webpack 插件

编写插件

Webpack 插件是一个具有 apply 方法的 JS 对象。 apply 方法会被 Webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象

// RemoveConsolePlugin.js
const pluginName = 'RemoveConsolePlugin';

class RemoveConsolePlugin {

// 由于不需要从外部传入 options
// 因此这里就不显示地定义 constructor 了
// constructor (options) {...}

apply(compiler) {
    compiler.hooks.emit.tapAsync(
      pluginName,
      (compilation, callback) => {
        Object.keys(compilation.assets).forEach((filename) => {
          // 仅处理 .js 文件
          if (filename.endsWith(".js")) {
            const asset = compilation.assets[filename];
            let content = asset.source();

            // 使用正则表达式移除整个 console.log 语句
            // 匹配 console.log( 之后的任意字符,直到遇到闭合的括号
            const consoleLogRegex = new RegExp(
              "console\\.log\\(.*?\\)",
              "g"
            );

            const withoutConsole = content.replace(consoleLogRegex, "");

            // 更新资源
            compilation.assets[filename] = {
              source: () => withoutConsole,
              size: () => Buffer.byteLength(withoutConsole, "utf8"),
            };
          }
        });

        callback();
      }
    );
  }
}

module.exports = RemoveConsolePlugin;

compiler hook 的 tap 方法的第一个参数,应该是大驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中重复使用

compiler.hooks.emit:可以通过 compiler 去获取一些 hook,在这里选择 emit 这个 hook:

![[Pasted image 20250711105004.png]]

在 asset 被输出到 output 之前,完成对 console.log 语句的删除

compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。它会对应用程序的依赖图中所有模块,进行字面上的编译 (literal compilation)

在这里,通过 compilation 实例获取到 assets,这里面就存着所有被处理的文件了。考虑到插件的运行会影响打包的速度,这里仅对 .js 文件做删除 console.log 语句的处理

通过调用 asset.source() 来获取文件的源代码,之后就是很熟悉的字符串的正则匹配和替换了

在 Webpack 配置中使用插件

回到 webpack.config.js 文件中,引入 RemoveConsolePlugin 并添加到插件数组中:

// webpack.config.js
const RemoveConsolePlugin = require("../src/RemoveConsolePlugin");

module.exports = {
  // ...其他配置...
  plugins: [
    new RemoveConsolePlugin(),
    // ...其他插件...
  ],
};

问题修复

运行之后发现 ouput 有问题:

![[Pasted image 20250711112140.png]]

console.log 语句确实是被删除了,但是留下了一堆逗号,导致打包后的文件异常了

先取消使用自定义插件,再打包一次看看:

![[Pasted image 20250711112231.png]]

可以看到,输出的文件里,每一行 console.log 语句后面都跟着一个逗号。所以使用正则的方式去删除 console.log 语句,还得给正则表达式加上一个是否以逗号结尾的匹配规则:(,|$)

const consoleLogRegex = new RegExp(
  "console\\.log\\(.*?\\)(,|$)",
  "g"
);

这样就解决了这个问题

扩展思考